﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using NesEmulatorX.Exceptions;
using NesEmulatorX.Util;

namespace NesEmulatorX
{
	public class NESROM
	{
		#region Constants

		public const string NESKey = "NES";
		public const int NESMagicNumber = 0x1A; // I think this is just a DOS new line character ('\0' or something of the like)

		#endregion

		#region Properties

		public string Key
		{
			get { return _key; }
			set { _key = value; }
		}
		private string _key = String.Empty;

		public byte MagicNumber
		{
			get { return _magicNumber; }
			set { _magicNumber = value; }
		}
		private byte _magicNumber = 0;

		public int PRGROMPageCount
		{
			get { return _pRGROMPageCount; }
			set { _pRGROMPageCount = value; }
		}
		private int _pRGROMPageCount = 0;

		public int CHRROMPageCount
		{
			get { return _cHRROMPageCount; }
			set { _cHRROMPageCount = value; }
		}
		private int _cHRROMPageCount = 0;

		public bool IsVerticalMirroring
		{
			get { return _isVerticalMirroring; }
			set { _isVerticalMirroring = value; }
		}
		private bool _isVerticalMirroring = false; // defaults to Horizontal mirroring

		public bool IsSRAMEnabled
		{
			get { return _isSRAMEnabled; }
			set { _isSRAMEnabled = value; }
		}
		private bool _isSRAMEnabled = false;

		public bool IsTrainerPresent
		{
			get { return _isTrainerPresent; }
			set { _isTrainerPresent = value; }
		}
		private bool _isTrainerPresent = false;

		public bool IsFourScreenMirroringEnabled
		{
			get { return _isFourScreenMirroringEnabled; }
			set { _isFourScreenMirroringEnabled = value; }
		}
		private bool _isFourScreenMirroringEnabled = false;

		public int MapperNumber
		{
			get { return _mapperNumber; }
			set { _mapperNumber = value; }
		}
		private int _mapperNumber = 0;

		public byte[] TrainerPage
		{
			get { return _trainerPage; }
			set { _trainerPage = value; }
		}
		private byte[] _trainerPage = new byte[0];

		public List<byte[]> PRGROMPages
		{
			get { return _prgROMPages; }
			set { _prgROMPages = value; }
		}
		private List<byte[]> _prgROMPages = new List<byte[]>();

		public List<byte[]> CHRROMPages
		{
			get { return _chrROMPages; }
			set { _chrROMPages = value; }
		}
		private List<byte[]> _chrROMPages = new List<byte[]>();

		private byte[] Data
		{
			get { return _data; }
			set { _data = value; }
		}
		private byte[] _data = null;

		private int ReadIndex
		{
			get { return _readIndex; }
			set { _readIndex = value; }
		}
		private int _readIndex = 0;

		#endregion

		#region Public Methods

		public bool Load(byte[] data)
		{
			if (data == null)
				throw new ArgumentNullException("data");
			if (data.Length == 0)
				throw new ArgumentException("data");

			this.ReadIndex = 0;
			this.Data = data;

			this.PRGROMPages.Clear();

			this.LoadNESKey();
			this.LoadNESMagicNumber();
			this.LoadPRGROMPageCount();
			this.LoadCHRROMPageCount();

			byte mapperLower = this.LoadROMControlByte1();
			byte mapperUpper = this.LoadROMControlByte2();

			this.MapperNumber = (mapperUpper << 4) | mapperLower;

			// blank data until 0xF (15), so start at 0x10 (16)
			this.ReadIndex = 0x10;

			if (this.IsTrainerPresent)
				this.ReadTrainer();

			for (int i = 0; i < this.PRGROMPageCount; i++)
				this.PRGROMPages.Add(new byte[NESEmulator.PRG_ROM_SIZE]);

			this.ReadPRGROMPages();

			for (int i = 0; i < this.CHRROMPageCount; i++)
				this.CHRROMPages.Add(new byte[NESEmulator.CHR_ROM_SIZE]);

			this.ReadCHRROMPages();

			return true;
		}

		public int GetPRGROMPageStartAddress(int page)
		{
			int offset = this.IsTrainerPresent ? NESEmulator.PRG_TRAINER_SIZE : 0;

			return 0x10 + offset + (page * NESEmulator.PRG_ROM_SIZE); // 0x10 will be the first byte of the new page (if no trainer)
		}

		public int GetCHRROMPageStartAddress(int page)
		{
			return this.GetPRGROMPageStartAddress(this.PRGROMPageCount - 1) +
				   NESEmulator.PRG_ROM_SIZE + // get passed the last PRG-ROM page
				   (page * NESEmulator.CHR_ROM_SIZE);
		}

		#endregion

		#region Private Methods

		private void LoadNESKey()
		{
			this.Key = this.ReadString(NESROM.NESKey.Length);

			if (this.Key != "NES")
				throw new NESROMInvalidNESKeyException();
		}

		private void LoadNESMagicNumber()
		{
			this.MagicNumber = this.ReadByte();

			if (this.MagicNumber != NESROM.NESMagicNumber)
				throw new NESROMInvalidNESMagicNumberException();
		}

		private void LoadPRGROMPageCount()
		{
			this.PRGROMPageCount = this.ReadByte();
		}

		private void LoadCHRROMPageCount()
		{
			this.CHRROMPageCount = this.ReadByte();
		}

		private byte LoadROMControlByte1()
		{
			byte b = this.ReadByte();

			this.LoadHorizontalVerticalMirroringEnabled(b);
			this.LoadSRAMEnabled(b);
			this.LoadTrainerPresent(b);
			this.LoadFourScreenMirroringEnabled(b);

			// the lower nibble of the mapper number
			return Binary.UpperNibble(b);
		}

		private void LoadHorizontalVerticalMirroringEnabled(byte b)
		{
			this.IsVerticalMirroring = Binary.Bit8(b) > 0;
		}

		private void LoadSRAMEnabled(byte b)
		{
			this.IsSRAMEnabled = Binary.Bit7(b) > 0;
		}

		private void LoadTrainerPresent(byte b)
		{
			this.IsTrainerPresent = Binary.Bit6(b) > 0;
		}

		private void LoadFourScreenMirroringEnabled(byte b)
		{
			this.IsFourScreenMirroringEnabled = Binary.Bit5(b) > 0;
		}

		private byte LoadROMControlByte2()
		{
			byte b = this.ReadByte();

			// the upper nibble of the mapper number
			return Binary.UpperNibble(b);
		}

		private void ReadTrainer()
		{
			this.TrainerPage = new byte[NESEmulator.PRG_TRAINER_SIZE];

			var data = this.ReadBytes(NESEmulator.PRG_TRAINER_SIZE);
			data.CopyTo(this.TrainerPage, 0);
		}

		private void ReadPRGROMPages()
		{
			for (int i = 0; i < this.PRGROMPageCount; i++)
			{
				var data = this.ReadBytes(NESEmulator.PRG_ROM_SIZE);
				data.CopyTo(this.PRGROMPages[i], 0);
			}
		}

		private void ReadCHRROMPages()
		{
			for (int i = 0; i < this.CHRROMPageCount; i++)
			{
				var data = this.ReadBytes(NESEmulator.CHR_ROM_SIZE);
				data.CopyTo(this.CHRROMPages[i], 0);
			}
		}

		private string ReadString(int length)
		{
			string s = ASCIIEncoding.ASCII.GetString(this.Data, this.ReadIndex, length);

			this.ReadIndex += sizeof(byte) * length; // byte being an ASCII char. A .NET char is 2 bytes as it is UNICODE

			return s;
		}

		private byte ReadByte()
		{
			byte b = this.Data[this.ReadIndex];

			this.ReadIndex += sizeof(byte) * 1;

			return b;
		}

		private byte[] ReadBytes(int count)
		{
			var bytes = this.Data.Skip(this.ReadIndex).Take(count).ToArray();

			this.ReadIndex += sizeof(byte) * count;

			return bytes;
		}

		#endregion
	}
}
